package yolo

import (
	"appengine"
	"appengine/datastore"
	"appengine/taskqueue"
	"fmt"
	"net/http"
	"strings"
	"time"
)

func init() {
	// Poller task handlers: it takes key=PollContext.Key(c) parameter and
	// stops running if PollContext.Stopped is marked true.
	http.HandleFunc("/worker/poller_run_task", handlePollerRunTask)

	// Start next poll with new ModifiedAfter.
	// If mode is PollModeBackdate and "update_timerange" parameter is specified,
	// this automatically shifts the poll time range to
	// [old_ModifiedAfter - BackdateTimeRange, old_ModifiedAfter].
	// This will fail (and will be retried) if there're pending tasks.
	http.HandleFunc("/worker/poller_next_poll", handlePollerNextPoll)

	RegisterCacheableType(PollContext{})
}

func handlePollerNextPoll(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	key := r.FormValue("key")
	pc, err := GetPollContext(c, key)
	if err != nil {
		c.Errorf("NextPoll: No poll context found for: %s", key)
		return
	}
	if pc.Stopped {
		c.Infof("NextPoll: PollContext is marked stopped: %s", pc.Name())
		return
	}
	pending, _ := pc.getPendingCounter(c).Read(c)
	if pending != 0 {
		// Throttling: make it retry.
		http.Error(w, "Too many pending tasks", http.StatusServiceUnavailable)
		return
	}
	if pc.Mode == PollModeBackdate && r.FormValue("update_timerange") != "" {
		old_after := pc.ModifiedAfter
		pc.ModifiedAfter = pc.ModifiedAfter.Add(-pc.BackdateTimeRange)
		pc.ModifiedBefore = old_after
		pc.Store(c, true)
		c.Debugf("NextPoll: %s updated time range (%s,%s)", pc.Name(),
				pc.ModifiedAfter.Format(time.RFC3339),
				pc.ModifiedBefore.Format(time.RFC3339))
	}
	err = pc.GetPoller(c).Start(c)
	if err != nil {
		c.Errorf("NextPoll: %s start failed: %s", pc.Name(), err.Error())
	}
}

func handlePollerRunTask(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	key := r.FormValue("key")

	siteurl := r.FormValue("siteurl")
	if key == "" && siteurl != "" {
		poller := GetPoller(c, siteurl)
		if poller == nil {
			c.Errorf("RunTask: No poller found for: %s", siteurl)
		}
		poller.RunTask(c, r)
		return
	}

	pc, err := GetPollContext(c, key)
	if err != nil {
		c.Errorf("RunTask: No poll context found for: %s", key)
		return
	}
	pc.getPendingCounter(c).Decrement(c)
	if pc.Stopped {
		c.Infof("RunTask: PollContext is marked stopped: %s", pc.Name())
		return
	}
	err = pc.GetPoller(c).RunTask(c, r)
	if err != nil {
		c.Errorf("RunTask: %s RunTask returned: %s", pc.Name(), err.Error())
		return
	}
}

type PollMode int

const (
	PollModeUnknown PollMode = iota
	PollModeUpdate
	PollModeBackdate
)

type PollContext struct {
	SiteURL        string
	Args           []string
	Created        time.Time
	ModifiedAfter  time.Time
	ModifiedBefore time.Time
	Mode           PollMode
	Stopped        bool
	Processed      int
	BackdateTimeRange time.Duration
}

func (pc *PollContext) Name() string {
	return fmt.Sprintf("%s-%v-%s", pc.SiteURL, pc.Mode, pc.Created.Format("2006-01-02T15-04-05"))
}

func (pc *PollContext) Key(c appengine.Context) *datastore.Key {
	id := fmt.Sprintf("%s-%v-%v", pc.SiteURL, pc.Mode, pc.Created.Unix() / 1000000)
	return datastore.NewKey(c, "PollContext", id, 0, nil)
}

func (pc *PollContext) stringKey(c appengine.Context) string {
	return pc.Key(c).Encode()
}

func (pc *PollContext) Store(c appengine.Context, immediate bool) error {
	return StoreCachedObject(c, pc.stringKey(c), pc, immediate)
}

func (pc *PollContext) Delete(c appengine.Context) error {
	pc.GetPoller(c).Delete(c)
	pc.getPendingCounter(c).Delete(c)
	return DeleteCachedObject(c, pc.stringKey(c))
}

func (pc *PollContext) GetPoller(c appengine.Context) Poller {
	f, _ := pollers[pc.SiteURL]
	if f != nil {
		return f(pc, pc.Args)
	}
	return new(NullPoller)
}

func (pc *PollContext) PostNextPoll(c appengine.Context, delay time.Duration, updateTimerange bool) error {
	key := pc.stringKey(c)
	params := map[string][]string {"key": {key}}
	if updateTimerange {
		params["update_timerange"] = []string{"1"}
	}
	t := taskqueue.NewPOSTTask("/worker/poller_next_poll", params)
	if delay >= 0 {
		t.Delay = delay
	}
	t.Name = fmt.Sprintf("poller-start-%s-%v", key, time.Now().UnixNano())
	_, err := taskqueue.Add(c, t, "poller")
	if err != nil {
		c.Infof("%s: Post error: %s", currentLocation(), err.Error())
	}
	return err
}

func (pc *PollContext) PostTask(c appengine.Context, name string, delay time.Duration, additionalParams map[string][]string) error {
	key := pc.Key(c).Encode()
	params := map[string][]string {"key": {key}}
	for k, v := range additionalParams {
		params[k] = v
	}
	t := taskqueue.NewPOSTTask("/worker/poller_run_task", params)
	if delay >= 0 {
		t.Delay = delay
	}
	if name != "" {
		t.Name = fmt.Sprintf("poller-task-%s-%s", key, name)
	}
	_, err := taskqueue.Add(c, t, "default")
	if err != nil {
		pc.getPendingCounter(c).Increment(c)
	}
	return err
}

func (pc *PollContext) RecordProgress(c appengine.Context, processed int, newModifiedAfter time.Time) error {
	fn := func(v interface{}) error {
		pc := v.(*PollContext)
		pc.Processed += processed
		if newModifiedAfter.After(pc.ModifiedAfter) && pc.Mode == PollModeUpdate {
			pc.ModifiedAfter = newModifiedAfter
		}
		return nil
	}
	key := pc.stringKey(c)
	err := UpdateCachedObject(c, key, pc, fn, false)
	if err != nil {
		c.Errorf("Update %s failed: %s", key, err.Error())
		return err
	}
	if newModifiedAfter.Unix() != 0 {
		c.Infof("PollContext %s updated modified time to: %s", pc.Name(), newModifiedAfter.Format(time.RFC3339))
	}
	return nil
}

func (pc *PollContext) getPendingCounter(c appengine.Context) *Counter {
	return &Counter{fmt.Sprintf("PollerPending-%s", pc.stringKey(c))}
}

// Poller interface.
type Poller interface {
	Start(c appengine.Context) error
	Delete(c appengine.Context) error
	RunTask(c appengine.Context, r *http.Request) error
}

var pollers = make(map[string]func(pc *PollContext, args []string) Poller)

func HasPoller(site string) bool {
	_, ok := pollers[site]
	return ok
}

func RegisterPoller(site string, f func(pc *PollContext) Poller) {
	RegisterPollerWithArguments(site, func (pc *PollContext, args []string) Poller { return f(pc) })
}

func RegisterPollerWithArguments(site string, f func(pc *PollContext, args []string) Poller) {
	pollers[site] = f
}

func AddPollContext(c appengine.Context, pc *PollContext) error {
	err := StoreCachedObject(c, pc.stringKey(c), pc, true)
	if err != nil {
		return err
	}
	return pc.GetPoller(c).Start(c)
}

func GetPollContexts(c appengine.Context, contexts interface{}) ([]*datastore.Key, error) {
	q := datastore.NewQuery("PollContext")
	return q.GetAll(c, contexts)
}

func StartPollContext(c appengine.Context, key string, forceStart bool) error {
	pc, err := GetPollContext(c, key)
	if err != nil {
		return err
	}
	if pc.Stopped {
		if !forceStart {
			c.Infof("PollContext is marked stopped: %s", pc.Name())
			return nil
		}
		pc.Stopped = false
		pc.Store(c, true)
	}

	return pc.GetPoller(c).Start(c)
}

func StopPollContext(c appengine.Context, key string) error {
	pc, err := GetPollContext(c, key)
	if err != nil {
		return err
	}
	pc.Stopped = true
	return pc.Store(c, true)
}

func DeletePollContext(c appengine.Context, key string) error {
	c.Debugf("Deleting PollContext:" + key)
	pc, err := GetPollContext(c, key)
	if err != nil {
		return err
	}
	return pc.Delete(c)
}

func GetPollContext(c appengine.Context, key string) (*PollContext, error) {
	pc := new(PollContext)
	err := GetCachedObject(c, key, pc)
	return pc, err
}

func GetPoller(c appengine.Context, siteurl string) Poller {
	sitedata := strings.Split(siteurl, "/")
	args := make([]string, 0)
	if len(sitedata) > 1 {
		siteurl = sitedata[0]
		args = sitedata[1:]
	}
	f, _ := pollers[siteurl]
	pc := &PollContext{}
	pc.SiteURL = siteurl
	pc.Args = args
	if f != nil {
		return f(pc, args)
	}
	return nil
}

type NullPoller struct {}

func (this *NullPoller) Start(c appengine.Context) error {
	return NewYoloError("NullPoller.Start() called")
}

func (this *NullPoller) Delete(c appengine.Context) error {
	return NewYoloError("NullPoller.Delete() called")
}

func (this *NullPoller) RunTask(c appengine.Context, r *http.Request) error {
	return NewYoloError("NullPoller.RunTask() called")
}
